---
title: Client Side Navigation (Single Page Apps)
description: Implement client side navigation in your RedwoodSDK project
---

import { Aside, Badge } from '@astrojs/starlight/components';

## What is Client Side Navigation?

Client-side navigation is a technique that allows users to move between pages without a full-page reload. Instead of the browser reloading the entire HTML document, the JavaScript runtime intercepts navigation events (like link clicks), fetches the next page's content (usually as JavaScript modules or RSC payload), and updates the current view.

This approach is commonly referred to as a Single Page App (SPA). In RedwoodSDK, you get SPA-like navigation with server-fetched React Server Components (RSC), so it's fast and dynamic, but still uses the server for rendering.

```tsx title="src/client.tsx" ins="initClientNavigation()"
import { initClient, initClientNavigation } from "rwsdk/client";

const { handleResponse } = initClientNavigation();
initClient({ handleResponse });
```

Once this is initialized, internal `<a href="/some-path">` links will no longer trigger full-page reloads. Instead, the SDK will:

1. Intercept the link click,
2. Push the new URL to the browser's history,
3. Fetch the new page's RSC payload from the server,
4. And hydrate it on the client.

RedwoodSDK keeps everything minimal and transparent. No magic routing system. No nested router contexts. You get the benefits of a modern SPA without giving up control.

## Transitions and View Animations

Client-side navigation enables you to animate between pages without jank. Pair it with View Transitions in React 19 to create seamless visual transitions.

## Caveats

No routing system is included: RedwoodSDK doesn't provide a client-side router. You can layer your own state management or page transitions as needed.

Only internal links are intercepted: RedwoodSDK will only handle links pointing to the same origin. External links (https://example.com) or those with `target="\_blank"` behave normally.

Middleware still runs: Every navigation hits your server again — so auth checks, headers, and streaming behavior remain intact.

## Configuring Scroll Behaviour

By default RedwoodSDK jumps to the **top** of the new page the moment the
content finishes rendering – just like a traditional full page load.

If you would like a different experience you can adjust it with
`initClientNavigation`:

```tsx title="Smooth scroll"
import { initClientNavigation } from "rwsdk/client";

initClientNavigation({
  scrollBehavior: "smooth",
});
```

### Disable automatic scrolling

For infinite-scroll feeds or chat applications you might want to _keep_ the user
exactly where they were, by settting `history.scrollRestoration` to `"manual"` before each navigation action you'll disable the automatic scrolling.

```tsx title="src/client.tsx" ins="history.scrollRestoration = 'manual'"
history.scrollRestoration = "manual";
```

Alternatively you can set `scrollToTop: false` to disable it completely.

```tsx title="src/client.tsx" ins="scrollToTop: false"
initClientNavigation({
  scrollToTop: false,
});
```

### Advanced: custom navigation callback

Need to run analytics or state updates before the request is sent? Provide your
own `onNavigate` handler:

```tsx
initClientNavigation({
  scrollBehavior: "auto",
  onNavigate: async () => {
    await analytics.track("page_view", { path: window.location.pathname });
  },
});
```

### Best Practices

- Use the default instant jump for content-heavy pages – it feels identical to a
  classic navigation and is the least surprising.
- Prefer `scrollBehavior: "smooth"` for marketing sites where visual polish is
  important.
- Set `scrollToTop: false` for timelines or lists that the user is expected to
  scroll through continuously.

That’s it! No additional code or router configuration required – RedwoodSDK
watches for DOM updates and performs the scroll automatically.

## Programmatic Navigation

While intercepting link clicks covers most navigation needs, you sometimes need to navigate programmatically - after a form submission, login event, or other user action.

The `navigate` function allows you to trigger navigation from anywhere in your code:

```tsx title="Navigate after form submission"
import { navigate } from "rwsdk/client";

function handleFormSubmit(event: FormEvent) {
  event.preventDefault();
  
  navigate("/dashboard");
}
```

```tsx title="Redirect after login, replacing history"
import { navigate } from "rwsdk/client";

async function handleLogin(credentials: Credentials) {
  await loginUser(credentials);
  
  navigate("/account", { history: "replace" });
}
```

```tsx title="Navigate with custom scroll behavior"
import { navigate } from "rwsdk/client";

function handleSpecialAction() {
  navigate("/results", {
    info: {
      scrollBehavior: "smooth",
      scrollToTop: true,
    },
  });
}
```

The `navigate` function accepts two parameters:

- `href`: The destination path
- `options`: An optional configuration object with:
  - `history`: Either `'push'` (default) to add a new history entry, or `'replace'` to replace the current one
  - `info.scrollToTop`: Whether to scroll to the top after navigation (default: `true`)
  - `info.scrollBehavior`: How to scroll - `'instant'` (default), `'smooth'`, or `'auto'`

## API Reference

### `initClientNavigation()` <Badge text="Experimental" type="caution" />

Initializes the client-side navigation. Call this function from your `client.tsx` entry point.
